package jocode;

import javax.media.opengl.*;
import java.io.File;
import java.awt.image.BufferedImage;
import java.lang.reflect.Method;

/**
 *  Make a high-resolution screenshot. Divides the current
 *  scene into a tiled grid, Each tile is rendered to the
 *  framebuffer and copied to a BufferedImage (screenshot()).
 *  <P>
 *  An alternative function (screenshotTiles()) saves the tiles to
 *  separate image files without stitching them together into a
 *  BufferedImage.  This reduces memory usage, allowing for larger
 *  screenshots, but requires that the images be stitched together by
 *  a utility app.
 *  <P>
 *  This class expects a reference to the parent application (extends JOApp),
 *  and a pointer to the function that draws the scene (usually will be draw()).
 *  <PRE>
 *            drawScene()     // draws the scene
 *  </PRE>
 *  This class also calls JOApp.screenShot() and JOApp.makeTimestamp()
 *  <P>
 *  To use:
 *  <PRE>
 *      MyJOApp extends JOApp {
 *          JOGiantScreenShot gss;
 *          setup() {
 *              gss = new JOGiantScreenShot(
 *                        this, method("drawScene"), "big_images",
 *                        55f, (float)DM.getWidth()/(float)DM.getHeight(), 1f, 100f,    // same as gluPerspective()
 *                        DM.getWidth(), DM.getHeight());
 *              ...
 *              gl.glMatrixMode(GL.GL_PROJECTION);
 *              gl.glLoadIdentity();
 *              glu(55f,aspectRatio,1f,100f);  // same as JOGiantScreenShot()
 *          }
 *          drawScene() {
 *              // draw the scene here
 *          }
 *          keyDown(int keycode) {
 *  	        if (keycode == Keyboard.KEY_F1) {
 *  		        gss.screenShot(8);    // eight times screen size
 *    	        }
 *          }
 *      }
 *  </PRE>
 *  NOTE:<BR>
 *  -- the drawScene() function should not change the Projection matrix, since GSS sets
 *     Projection to render a piece of the scene for each tile (don't call glFrustum(), glOrtho()
 *     or gluPerspective() inside drawScene())<BR>
 *  -- drawScene() should clear the colorbuffer each time it's called, otherwise previous
 *     tiles will overlay in the colorbuffer.  This means that GLGiantScreenshot won't work
 *     with apps that leave trails on the screen by not clearing the framebuffer.<BR>
 *  -- to use GLGiantScreenShot with ortho mode, create the GLGiantScreenShot object then
 *     call gss.setOrtho(...) with the same params that you used in your ortho mode setup.
 *  -- The screenShot() function requires a lot of memory to create a large BufferedImage.  Run
 *     java with more memory to avoid OutOfMemory errors:  java -Xms128m -Xmx128m
 */
public class JOGiantScreenShot
{
    private JOApp mainApp;
    private float Left, Right, Bottom, Top, Near, Far;  // frustum dimensions of scene
    private int screenW, screenH;
    private int screenshotCounter;
    private boolean orthomode = false;
    private Method drawSceneFunc;               // method that will draw the scene
    private String screenshotFolder = "";       // folder where image tiles are saved
    private String imagePrefix = "gss_";        // prefix added to image file name
    private boolean doingScreenshot = false;    // flag is true when screenshot is in progress


	/**
	 *  Create a GiantScreenShot object.
	 *  Pass the parent application.  Parent app must have a renderFrame() function
	 *  that draws the scene.
	 *  The params fovy, aspect, znear and zfar should be the same values that your
	 *  app gives to gluPerspective().
	 */
	public JOGiantScreenShot(JOApp application, Method drawSceneFunction, String outputFolder,
							 float fovy, float aspect, float zNear, float zFar, // perspective settings
							 int displayWidth, int displayHeight)   // current DisplayMode
	{
		mainApp = application;
		setFolder(outputFolder);
		screenW = displayWidth;
		screenH = displayHeight;
		drawSceneFunc = drawSceneFunction;
		setPerspective(fovy, aspect, zNear, zFar);
	}

    /**
     *  Called by constructor to initialize frustum settings.
     *  Use same params as for gluPerspective().
     */
    public void setPerspective(float fovy, float aspect, float zNear, float zFar)
    {
       Top    = zNear * (float)Math.tan(fovy * 3.14159265 / 360.0);
       Bottom = -Top;
       Left   = Bottom * aspect;
       Right  = Top * aspect;
       Near   = zNear;
       Far    = zFar;
       orthomode = false;
    }

    /**
     *  Call setOrtho() to switch to orthogonal (2D) instead of perspective.
     *  Use same params as for glFrustum().
     */
    public void setOrtho(float left, float right, float bottom, float top, float zNear, float zFar)
    {
       Top    = top;
       Bottom = bottom;
       Left   = left;
       Right  = right;
       Near   = zNear;
       Far    = zFar;
       orthomode = true;
    }

    /**
     *  Set the output folder for screenshot files.  If outputFolder param is null
     *  or empty string, the current folder will be used.
     */
    public void setFolder(String outputFolder)
    {
    	if (outputFolder == null) {
			outputFolder = "";
		}
    	if (!outputFolder.equals("") && !outputFolder.substring(outputFolder.length()).equals("/")) {
			outputFolder = outputFolder + "/";
		}
		screenshotFolder = outputFolder;
    }

    /**
     *  Set the output file prefix for screenshot files.  If prefix param is null
     *  then there will be no prefix (file name will be just the current datetime).
     */
    public void setPrefix(String filenamePrefix)
    {
    	if (filenamePrefix == null) {
			filenamePrefix = "";
		}
		imagePrefix = filenamePrefix;
    }

    /**
     * Create an image of the scene <scale> times larger than the screen.
     * <P>
     * Renders the scene as a grid of tiles.  Each tile is rendered to the screen
     * and the frame pixels are copied into a large BufferedImage.  A scale parameter
     * of 8 produces an image 8 times larger than the screen (will draw the scene as
     * a grid of 8 rows and 8 columns where each grid tile is the size of the screen).
     *
     * @param scale  should divide evenly into screen width and height (ie. 4, 8, 16)
     */
    public void screenshot(int scale)
    {
        String timestamp = JOApp.makeTimestamp();
        int imageWidth  = screenW;
        int imageHeight = screenH;
        int tileWidth   = screenW / scale;
        int tileHeight  = screenH / scale;
        BufferedImage finalImg = null;
        int[] pixelsARGB = new int[screenW * screenH];

        // Create a BufferedImage to hold the large scene image
        try {
            finalImg = new BufferedImage(screenW*scale, screenH*scale, BufferedImage.TYPE_INT_RGB);
        }
        catch (Exception e) {
            JOApp.msg("JOGiantScreenShot.screenshot(): Could not create bufferedimage with size ("+(screenW*scale)+","+(screenH*scale)+"): exception " + e);
            return;
        }

        doingScreenshot = true;

        // preserve the current perspective view
        JOApp.gl.glMatrixMode(GL.GL_PROJECTION);
        JOApp.gl.glPushMatrix();
        JOApp.gl.glMatrixMode(GL.GL_MODELVIEW);

        // for each tile,
        for (int r=0; r < scale; r++) {
            for (int c=0; c < scale; c++) {
				// adjust frustum to show one chunk of scene,
                setFrustum(c, r, tileWidth, tileHeight, imageWidth, imageHeight);
                // call function to draw the scene
                JOApp.invoke(mainApp,drawSceneFunc);
                // copy frame pixels into an int array and flip the Y axis
                pixelsARGB = JOApp.framePixelsInt(0, 0, screenW, screenH);
                pixelsARGB = JOImage.flipPixels(pixelsARGB,screenW,screenH);
				// put pixels into BufferedImage
            	int invertedRow = (scale - r) - 1;
                try {
                    finalImg.setRGB(c*screenW, invertedRow*screenH, screenW, screenH, pixelsARGB, 0, screenW);
                }
                catch (Exception e) {
                    JOApp.msg("GLGiantScreenShot.screenshot(): set bufferedimage pixels: exception " + e);
                }
            }
        }
        // return to previous perspective
        JOApp.gl.glMatrixMode(GL.GL_PROJECTION);
        JOApp.gl.glPopMatrix();
        JOApp.gl.glMatrixMode(GL.GL_MODELVIEW);

		// save finalImg to PNG
		JOApp.msg("GLGiantScreenShot.screenshot(): Save image: " + screenshotFolder+imagePrefix+timestamp+".png");
		try {
			javax.imageio.ImageIO.write(finalImg, "png", new File(screenshotFolder+imagePrefix+timestamp+".png"));
		}
		catch (Exception e) {
            JOApp.msg("JOGiantScreenShot.screenshot(): Error when saving BufferedImage: " + e);
		}

        doingScreenshot = false;
    }

    /**
     * Render the scene as a grid of tiles.  Each tile will be the size of the screen,
     * and will be saved to a separate file.  The scale param is the number of tiles
     * per row and column. ie. scale=8 will produce a grid of 8 rows and 8 columns
     * for a total of 64 tile images. The final image will be 8x the size of the screen.
     *
     * @param scale  should divide evenly into screen width and height
     */
    public void screenshotTiles(int scale)
    {
        String timestamp = JOApp.makeTimestamp();
        int imageWidth  = screenW;
        int imageHeight = screenH;
        int tileWidth   = screenW / scale;
        int tileHeight  = screenH / scale;

        screenshotCounter = 0;

        JOApp.msg(" tileW=" + tileWidth + " tileH=" + tileHeight + " imgW=" + imageWidth + " imgH=" + imageHeight);

        // make a regular screenshot for reference
        JOApp.screenShot(0, 0, screenW, screenH, screenshotFolder+imagePrefix+timestamp+".png");
        // preserve the current perspective view
        JOApp.gl.glMatrixMode(GL.GL_PROJECTION);
        JOApp.gl.glPushMatrix();
        JOApp.gl.glMatrixMode(GL.GL_MODELVIEW);
        // for each tile, adjust frustum to show one chunk of scene, then render and save to file
        for (int r=0; r < scale; r++) {
            for (int c=0; c < scale; c++) {
                setFrustum(c, r, tileWidth, tileHeight, imageWidth, imageHeight);
                JOApp.invoke(mainApp,drawSceneFunc); // call function to draw the scene
                JOApp.screenShot(0, 0, screenW, screenH, screenshotFolder+imagePrefix+timestamp+"_"+screenshotCounter+".png");
                screenshotCounter++;
            }
        }
        // return to previous perspective
        JOApp.gl.glMatrixMode(GL.GL_PROJECTION);
        JOApp.gl.glPopMatrix();
        JOApp.gl.glMatrixMode(GL.GL_MODELVIEW);
    }

    /**
     * sets frustum to render one portion of much larger scene. See screenshot().
     */
    private void setFrustum(int CurrentColumn, int CurrentRow, int TileWidth, int TileHeight, int ImageWidth, int ImageHeight)
    {
        float left   = Left + (((Right - Left) * (CurrentColumn * TileWidth)) / ImageWidth);
        float right  = left + (((Right - Left) * TileWidth) / ImageWidth);
        float bottom = Bottom + (Top - Bottom) * (CurrentRow * TileHeight) / ImageHeight;
        float top    = bottom + (Top - Bottom) * TileHeight / ImageHeight;

        JOApp.gl.glMatrixMode(GL.GL_PROJECTION);
        JOApp.gl.glLoadIdentity();
        if (orthomode) {
        	JOApp.gl.glOrtho(left, right, bottom, top, Near, Far);
		}
		else {
        	JOApp.gl.glFrustum(left, right, bottom, top, Near, Far);
		}
        JOApp.gl.glMatrixMode(GL.GL_MODELVIEW);
    }

    /**
     * return true if a screenshot is in progress. The calling application can use this flag
     * to pause animation when rendering the giant screenshot (or objects might move from one
     * tile to the next).
     */
    public boolean doingScreenshot() {
    	return doingScreenshot;
    }
}
